Skip to content

fix: resolve Python absolute imports when the import root is a subdirectory - #91

Open
schergr wants to merge 1 commit into
glincker:masterfrom
schergr:fix/python-absolute-imports
Open

schergr wants to merge 1 commit into
glincker:masterfrom
schergr:fix/python-absolute-imports

Conversation

@schergr

@schergr schergr commented Nov 9, 2026

Copy link
Copy Markdown

What and why

In a monorepo where the Python package sits below the scanned root (for example apps/api/app/core/db.py, imported as from app.core.db import Session), the module graph has no edges and every module is listed as isolated. The Map tab shows unconnected circles, and dependencies.edges is empty.

resolveImport turns app.core.db into app/core/db and matches it to a module only when the import ends with the module name, starts with it, or ends with it (Go style). All three assume the module path is the shorter side. Here the module is apps/api/app/core and the import's leading app/core is the tail of that path. langResolver has Java, Ruby and Rust cases but no Python one.

This adds a Python case, modelled on the Java one:

  • each .py file is indexed under every trailing sub-path of its directory and of its own path without .py;
  • a dotted import resolves by its longest leading part that matches one of those tails, and the closest module wins when several match;
  • a single segment import only matches a package (a directory with an __init__.py), so import logging does not resolve to a repo file called logging.py;
  • relative imports and anything unmatched fall through to the existing generic resolver, so this only adds edges.

How it was checked

  • go vet ./... and go test ./... -race pass
  • Added or updated tests for behavior changes
  • Ran the built binary on a real repo (name it): a private Python monorepo with 1,201 files, 2,109 absolute from app. imports and 4 relative ones, laid out as apps/api/app/.... Module edges went from 0 to 418 and isolated modules from 89 to 14, with the same files and config. The symbol graph for the same repo already had 12,711 call edges and 2,266 import edges.

The two new tests in modresolve_test.go fail without the change (empty depended_by) and pass with it. They cover resolving through a file and through a package, a stdlib name that clashes with a repo file (logging), a third party name that matches a directory without an __init__.py (alembic), and single segment imports.

Docs

  • README, docs/USAGE.md or CHANGELOG.md updated if behavior changed (CHANGELOG entry under Unreleased, Fixed)
  • Any size or speed claim comes from go run ./bench output (no size or speed claims)
  • Planned or unmerged features are not described as shipped

🤖 Generated with Claude Code

…ectory

In a repo like apps/api/app/core/db.py imported as app.core.db, the module
graph had no edges and every module was listed as isolated. resolveImport only
matches when the module path is the shorter side, but here the import's leading
part is the tail of the module path.

Add a Python case to langResolver, modelled on the Java one. It indexes each
.py file under every trailing sub-path of its directory and of its own path,
resolves a dotted import by its longest matching leading part, and picks the
closest module when several match. A single segment import only matches a
package (a directory with an __init__.py), so import logging does not resolve
to a repo file called logging.py. Relative imports and anything unmatched still
go to the generic resolver, so this only adds edges.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-09T21:39:12.308031Z 66003dc PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 66003dcea1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +64 to +65
case ".py":
r.notePython(p, dir, mod, pySeen)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude skipped modules from the Python candidate index

When a scan contains pkg/__init__.py, app/main.py importing pkg, and app/tests/pkg/__init__.py, this registers the excluded test package as a candidate. closestModule selects app/tests/pkg, but Build discards it via shouldSkipModule(toMod) in graph.go:175-176; because resolution was already marked handled, the valid app -> pkg dependency disappears. The parent commit resolves this edge through knownModules, which excludes test modules. Filter skipped modules out before adding Python candidates.

Useful? React with 👍 / 👎.

Comment on lines +312 to +313
if mods := r.pyFiles[key]; n >= 2 && len(mods) > 0 {
return closestModule(p, mods)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prefer a package over a same-named Python file

If pkg/util.py and pkg/util/__init__.py coexist, an import pkg.util in consumer/main.py returns the file's containing module, pkg, here before considering the pkg/util package candidate. Python imports the package in this layout, and the parent commit's generic resolver also chose the exact pkg/util module. This therefore rewrites the dependency from consumer -> pkg/util to consumer -> pkg, producing an incorrect module graph. Prefer the matching package at the same import root before accepting the file.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant